﻿using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Text;
using System.Threading;
using System.Xml;
using RadaCode.SwissKnife.Random;
using RadaCode.SwissKnife.Search.Providers.Bing;

namespace RadaCode.SwissKnife.Search
{
    public class BingImageRetriever : IImageRetriever//, IDisposable
    {
        private MersenneTwister randomer;

        private const int several = 10;

        private BingPortTypeClient _bingClient;

        #region Search-specific fields

        private string _apiKey;
        private readonly ImageFormat _format;
        private readonly int _exactMaxOffset;

        #endregion

        private readonly IImageCacher _cacher;
        private readonly string _preCacheString;

        private Thread _cacheSeveralRandomBgrnd;
        private Thread _cacheSeveralExactBgrnd;

        #region Constructors/Dispose

        public BingImageRetriever(string apiKey, ImageFormat format, int exactMaxOffset = 30)
        {
            if (apiKey == string.Empty)
            {
                throw new ArgumentException("API key can not be empty!");
            }

            _exactMaxOffset = exactMaxOffset;
            _apiKey = apiKey;
            _format = format;

            randomer = new MersenneTwister();

            var defaultBinding = GetBingDefaultBinding();

            var bingEndpoint = new EndpointAddress(new Uri("http://api.search.live.net:80/soap.asmx"));

            _bingClient = new BingPortTypeClient(defaultBinding, bingEndpoint);
        }

        public BingImageRetriever(string apiKey, ImageFormat format, IImageCacher cacher, int exactMaxOffset = 30)
            : this(apiKey, format, exactMaxOffset)
        {
            _cacher = cacher;
            _cacheSeveralRandomBgrnd = new Thread(CacheSeveralRandomImagesUpFront);
            _cacheSeveralRandomBgrnd.Start();
            //CacheSeveralRandomImagesUpFront();
        }

        public BingImageRetriever(string apiKey, ImageFormat format, IImageCacher cacher, string preCacheString, int exactMaxOffset = 30)
            : this(apiKey, format, cacher, exactMaxOffset)
        {
            _preCacheString = preCacheString;
            _cacheSeveralExactBgrnd = new Thread(CacheSeveralImagesByKeywordUpFront);
            _cacheSeveralExactBgrnd.Start(_preCacheString);
            //CacheSeveralImagesByKeywordUpFront(_preCacheString);
        }

        //public void Dispose()
        //{
        //    if (_cacheSeveralRandomBgrnd != null)
        //    {
        //        if (_cacheSeveralRandomBgrnd.ThreadState != ThreadState.Stopped) _cacheSeveralRandomBgrnd.Join();
        //    }

        //    if (_cacheSeveralExactBgrnd != null)
        //    {
        //        if (_cacheSeveralExactBgrnd.ThreadState != ThreadState.Stopped) _cacheSeveralExactBgrnd.Join();
        //    }
        //}

        #endregion

        #region Public Funtionality

        public string GetRandomImage()
        {
            if (_cacher != null)
            {
                string randomImageFromCache;

                lock (_cacher)
                {
                    randomImageFromCache = _cacher.GetRandomImage();
                }

                if(randomImageFromCache != string.Empty)
                {
                    var bckGrounder = new Thread(CacheSeveralRandomImagesUpFront);
                    bckGrounder.Start();

                    return randomImageFromCache;
                }
            }

            var bingUnit = new BingUnitOfWork(_bingClient, _apiKey, _exactMaxOffset);

            ImageResult[] results = bingUnit.GetRandomImageResults(1);

            var url = results[0].MediaUrl;

            switch (_format)
            {
                case ImageFormat.Url:
                    return url;
                case ImageFormat.Binary:
                    return GetBinaryImageStringFromUrl(url);
                default: throw new ArgumentException("Unknown image result type specified");
            }
        }

        public string[] GetRandomImages(int imageCount)
        {
            if (_cacher != null)
            {
                string[] randomImageFromCache;

                lock (_cacher)
                {
                    randomImageFromCache = _cacher.GetRandomImages(imageCount);
                }

                if (randomImageFromCache.Length == imageCount)
                {
                    var bckGrounder = new Thread(CacheSeveralRandomImagesUpFront);
                    bckGrounder.Start();

                    return randomImageFromCache;
                }
            }

            var bingUnit = new BingUnitOfWork(_bingClient, _apiKey, _exactMaxOffset);

            ImageResult[] results = bingUnit.GetRandomImageResults(imageCount);

            var res = new string[imageCount];

            for (var i = 0; i < imageCount; i++)
            {
                res[i] = results[i].MediaUrl;
            }

            switch (_format)
            {
                case ImageFormat.Url:
                    return res;
                case ImageFormat.Binary:
                    return GetBinaryImageStringFromUrls(res);
                default: throw new ArgumentException("Unknown image result type specified");
            }
        }

        public string GetImageByKeyword(string keyword)
        {
            if (_cacher != null)
            {
                string keywordImageFromCache;

                lock (_cacher)
                {
                    keywordImageFromCache = _cacher.GetImageByKeyword(keyword);
                }

                if (keywordImageFromCache != string.Empty)
                {
                    var bckGrounder = new Thread(CacheSeveralImagesByKeywordUpFront);
                    bckGrounder.Start(keyword);

                    return keywordImageFromCache;
                }
            }

            var bingUnit = new BingUnitOfWork(_bingClient, _apiKey, _exactMaxOffset);

            ImageResult[] results = bingUnit.GetExactImageResults(keyword, 1);

            var url = results[randomer.Next(0, results.Length - 1)].MediaUrl;

            switch (_format)
            {
                case ImageFormat.Url:
                    return url;
                case ImageFormat.Binary:
                    return GetBinaryImageStringFromUrl(url);
                default: throw new ArgumentException("Unknown image result type specified");
            }
        }

        public string[] GetImagesByKeyword(string keyword, int imageCount)
        {
            if (_cacher != null)
            {
                string[] keywordImagesFromCache;

                lock (_cacher)
                {
                    keywordImagesFromCache = _cacher.GetImagesByKeyword(imageCount, keyword);
                }

                if (keywordImagesFromCache.Length == imageCount)
                {
                    var bckGrounder = new Thread(CacheSeveralImagesByKeywordUpFront);
                    bckGrounder.Start(keyword);

                    return keywordImagesFromCache;
                }
            }

            var bingUnit = new BingUnitOfWork(_bingClient, _apiKey, _exactMaxOffset);

            ImageResult[] results = bingUnit.GetExactImageResults(keyword, imageCount);

            var res = new string[imageCount];

            for (var i = 0; i < imageCount; i++)
            {
                res[i] = results[randomer.Next(0, results.Length - 1)].MediaUrl;
            }

            switch (_format)
            {
                case ImageFormat.Url:
                    return res;
                case ImageFormat.Binary:
                    return GetBinaryImageStringFromUrls(res);
                default: throw new ArgumentException("Unknown image result type specified");
            }
        }
        
        #endregion
        
        #region Caching

        private void CacheSeveralImagesByKeywordUpFront(object keyword)
        {
            Debug.WriteLine(String.Format("Will try to guaranty that {0} {1} images are in cache.", several, keyword));

            if (SeveralImagesWithKeywordAreInCache((string)keyword)) return;

            var randomImages = GetImagesByKeyword((string)keyword, several);

            lock (_cacher)
            {
                _cacher.AddImagesAsKeyword(randomImages, (string)keyword);
            }
        }

        private void CacheSeveralRandomImagesUpFront()
        {
            Debug.WriteLine(String.Format("Will try to guaranty that {0} random are images.", several));
            
            if (SeveralRandomImagesAreInCache()) return;

            var randomImages = GetRandomImages(several);

            lock (_cacher)
            {
                _cacher.AddRandomImages(randomImages);
            }
        }

        private bool SeveralImagesWithKeywordAreInCache(string keyword)
        {
            int imagesInCache;

            lock (_cacher)
            {
               imagesInCache  = _cacher.GetCachedImageCountByKeyword(keyword);
            }

            return imagesInCache >= several;
        }

        private bool SeveralRandomImagesAreInCache()
        {
            int imagesInCache;

            lock (_cacher)
            {
                imagesInCache = _cacher.GetNumberOfRandomImagesInCache();
            }

            return imagesInCache >= several;
        }

        #endregion

        #region Initialization

        private BasicHttpBinding GetBingDefaultBinding()
        {
            return new BasicHttpBinding(BasicHttpSecurityMode.None)
            {
                Name = "BingPortBinding",
                CloseTimeout = TimeSpan.FromMinutes(1),
                OpenTimeout = TimeSpan.FromMinutes(1),
                ReceiveTimeout = TimeSpan.FromMinutes(10),
                SendTimeout = TimeSpan.FromMinutes(1),
                AllowCookies = false,
                BypassProxyOnLocal = false,
                HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
                MaxBufferSize = 65536,
                MaxBufferPoolSize = 524288,
                MaxReceivedMessageSize = 65536,
                MessageEncoding = WSMessageEncoding.Text,
                TextEncoding = Encoding.UTF8,
                TransferMode = TransferMode.Buffered,
                UseDefaultWebProxy = true,
                ReaderQuotas = new XmlDictionaryReaderQuotas
                {
                    MaxDepth = 32,
                    MaxStringContentLength = 8192,
                    MaxArrayLength = 16384,
                    MaxBytesPerRead = 4096,
                    MaxNameTableCharCount = 16384
                },
                Security = new BasicHttpSecurity
                {
                    Mode = BasicHttpSecurityMode.None,
                    Transport = new HttpTransportSecurity
                    {
                        ClientCredentialType =
                            HttpClientCredentialType.None,
                        ProxyCredentialType =
                            HttpProxyCredentialType.None,
                        Realm = ""
                    },
                    Message = new BasicHttpMessageSecurity
                    {
                        ClientCredentialType = BasicHttpMessageCredentialType.UserName,
                        AlgorithmSuite = SecurityAlgorithmSuite.Default
                    }
                }
            };
        }

        #endregion

        #region Conversions

        private string[] GetBinaryImageStringFromUrls(string[] urlArray)
        {
            for (var i = 0; i < urlArray.Length; i++)
            {
                var imageBytes = GetBytesFromUrl(urlArray[i]);

                var url = urlArray[i];

                string ext;

                try
                {
                    ext = url.Substring(url.LastIndexOf('.') + 1);
                }
                catch (Exception)
                {
                    ext = string.Empty;
                }

                if (ExtIsKnown(ext))
                {
                    var firstPart = String.Format(@"data:image/{0};base64,", ext);
                    urlArray[i] = firstPart + EncodeFile(imageBytes);
                }
                else
                {
                    urlArray[i] = @"data:image/jpg;base64," + EncodeFile(imageBytes);
                }
            }

            return urlArray;
        }

        private string GetBinaryImageStringFromUrl(string url)
        {
            var imageBytes = GetBytesFromUrl(url);

            string ext;

            try
            {
                ext = url.Substring(url.LastIndexOf('.') + 1);
            }
            catch (Exception)
            {
                ext = string.Empty;
            }

            if (ExtIsKnown(ext))
            {
                var firstPart = String.Format(@"data:image/{0};base64,", ext);
                return firstPart + EncodeFile(imageBytes);
            }
            else
            {
                return @"data:image/jpg;base64," + EncodeFile(imageBytes);
            }
        }

        private bool ExtIsKnown(string ext)
        {
            return ext == "png" || ext == "jpg" || ext == "gif" || ext == "svg" || ext == "jpeg";
        }

        private byte[] GetBytesFromUrl(string url)
        {
            byte[] b;
            HttpWebRequest myReq = (HttpWebRequest)System.Net.WebRequest.Create(url);

            System.Net.WebResponse myResp;

            try
            {
                myResp = myReq.GetResponse();
            }
            catch (WebException)
            {
                return GetBytesFromUrl("http://placehold.it/200x200");
            }

            Stream stream = myResp.GetResponseStream();

            using (BinaryReader br = new BinaryReader(stream))
            {
                b = br.ReadBytes(500000);
                br.Close();
            }
            myResp.Close();
            return b;
        }

        private string EncodeFile(byte[] data)
        {
            string base64String;
            try
            {
                base64String =
                  System.Convert.ToBase64String(data,
                                         0,
                                         data.Length);
            }
            catch (System.ArgumentNullException)
            {
                System.Console.WriteLine("Binary data array is null.");
                return string.Empty;
            }

            return base64String;
        }

        #endregion
    }
}